﻿using System;
using System.Text;
using System.Drawing;
using System.Text.RegularExpressions;
using System.Collections.Generic;

namespace FastColoredTextBoxNS
{

    /// <summary>
    /// Diapason of text chars
    /// </summary>
    public class Range
    {
        Place start;
        Place end;
        protected FastColoredTextBox tb;
        int preferedPos = -1;
        int updating = 0;

        /// <summary>
        /// Constructor
        /// </summary>
        public Range(FastColoredTextBox tb)
        {
            this.tb = tb;
        }

        public bool Contains(Place place)
        {
            if (place.iLine < Math.Min(start.iLine, end.iLine)) return false;
            if (place.iLine > Math.Max(start.iLine, end.iLine)) return false;

            Place s = start;
            Place e = end;

            if (s.iLine > e.iLine || (s.iLine == e.iLine && s.iChar > e.iChar))
            {
                var temp = s;
                s = e;
                e = temp;
            }

            if (place.iLine == s.iLine && place.iChar < s.iChar) return false;
            if (place.iLine == e.iLine && place.iChar >= e.iChar) return false;

            return true;
        }

        /// <summary>
        /// Select all chars of control
        /// </summary>
        public void SelectAll()
        {
            Start = new Place(0, 0);
            if (tb.lines.Count == 0)
                Start = new Place(0, 0);
            else
            {
                end = new Place(0, 0);
                start = new Place(tb.lines[tb.lines.Count - 1].Count, tb.lines.Count - 1);
            }
        }

        /// <summary>
        /// Start line and char position
        /// </summary>
        public Place Start
        {
            get { return start; }
            set
            {
                end = start = value;
                preferedPos = -1;
                OnSelectionChanged();
            }
        }

        /// <summary>
        /// Finish line and char position
        /// </summary>
        public Place End
        {
            get
            {
                return end;
            }
            set
            {
                end = value;
                OnSelectionChanged();
            }
        }

        /// <summary>
        /// Text of range
        /// </summary>
        public string Text
        {
            get
            {
                int fromLine = Math.Min(end.iLine, start.iLine);
                int toLine = Math.Max(end.iLine, start.iLine);
                int fromChar = FromX;
                int toChar = ToX;
                if (fromLine < 0) return null;
                //
                StringBuilder sb = new StringBuilder();
                for (int y = fromLine; y <= toLine; y++)
                {
                    int fromX = y == fromLine ? fromChar : 0;
                    int toX = y == toLine ? toChar - 1 : tb.lines[y].Count - 1;
                    for (int x = fromX; x <= toX; x++)
                        sb.Append(tb.lines[y][x].c);
                    if (y != toLine && fromLine != toLine)
                        sb.AppendLine();
                }
                return sb.ToString();
            }
        }

        internal void GetText(out string text, out List<Place> charIndexToPlace)
        {
            int fromLine = Math.Min(end.iLine, start.iLine);
            int toLine = Math.Max(end.iLine, start.iLine);
            int fromChar = FromX;
            int toChar = ToX;

            StringBuilder sb = new StringBuilder();
            charIndexToPlace = new List<Place>();
            if (fromLine >= 0)
            {
                for (int y = fromLine; y <= toLine; y++)
                {
                    int fromX = y == fromLine ? fromChar : 0;
                    int toX = y == toLine ? toChar - 1 : tb.lines[y].Count - 1;
                    for (int x = fromX; x <= toX; x++)
                    {
                        sb.Append(tb.lines[y][x].c);
                        charIndexToPlace.Add(new Place(x, y));
                    }
                    if (y != toLine && fromLine != toLine)
                    foreach (char c in Environment.NewLine)
                    {
                        sb.Append(c);
                        charIndexToPlace.Add(new Place(tb.lines[y].Count/*???*/, y));
                    }
                }
            }
            text = sb.ToString();
        }

        /// <summary>
        /// Returns first char after Start place
        /// </summary>
        public char CharAfterStart
        {
            get
            {
                if (Start.iChar >= tb.lines[Start.iLine].Count)
                    return '\n';
                else
                    return tb.lines[Start.iLine][Start.iChar].c;
            }
        }

        /// <summary>
        /// Returns first char before Start place
        /// </summary>
        public char CharBeforeStart
        {
            get
            {
                if (Start.iChar <= 0)
                    return '\n';
                else
                    return tb.lines[Start.iLine][Start.iChar - 1].c;
            }
        }

        /// <summary>
        /// Clone range
        /// </summary>
        /// <returns></returns>
        public Range Clone()
        {
            return (Range)MemberwiseClone();
        }

        /// <summary>
        /// Return minimum of end.X and start.X
        /// </summary>
        internal int FromX
        {
            get
            {
                if (end.iLine < start.iLine) return end.iChar;
                if (end.iLine > start.iLine) return start.iChar;
                return Math.Min(end.iChar, start.iChar);
            }
        }

        /// <summary>
        /// Return maximum of end.X and start.X
        /// </summary>
        internal int ToX
        {
            get
            {
                if (end.iLine < start.iLine) return start.iChar;
                if (end.iLine > start.iLine) return end.iChar;
                return Math.Max(end.iChar, start.iChar);
            }
        }

        internal void GoLeft(bool shift)
        {
            if (start.iChar != 0 || start.iLine != 0)
            {
                if (start.iChar > 0)
                    start.Offset(-1, 0);
                else
                    start = new Place(tb.lines[start.iLine - 1].Count, start.iLine - 1);
            }

            if (!shift)
                end = start;

            OnSelectionChanged();

            preferedPos = -1;
        }

        /// <summary>
        /// Move range right
        /// </summary>
        public bool GoRight()
        {
            Place prevStart = start;
            GoRight(false);
            return prevStart != start;
        }

        /// <summary>
        /// Move range left
        /// </summary>
        public bool GoLeft()
        {
            Place prevStart = start;
            GoLeft(false);
            return prevStart != start;
        }

        internal void GoRight(bool shift)
        {
            if (start.iLine < tb.lines.Count - 1 || start.iChar < tb.lines[tb.lines.Count - 1].Count)
            {
                if (start.iChar < tb.lines[start.iLine].Count)
                    start.Offset(1, 0);
                else
                    start = new Place(0, start.iLine + 1);
            }

            if (!shift)
                end = start;

            OnSelectionChanged();

            preferedPos = -1;
        }

        internal void GoUp(bool shift)
        {
            if (preferedPos < 0)
                preferedPos = start.iChar - tb.lines[start.iLine].GetWordWrapStringStartPosition(tb.lines[start.iLine].GetWordWrapStringIndex(start.iChar));

            int iWW = tb.lines[start.iLine].GetWordWrapStringIndex(start.iChar);
            if (iWW == 0)
            {
                if (start.iLine <= 0) return;
                start.iLine--;
                iWW = tb.lines[start.iLine].WordWrapStringsCount;
            }

            if (iWW > 0)
            {
                int finish = tb.lines[start.iLine].GetWordWrapStringFinishPosition(iWW - 1);
                start.iChar = tb.lines[start.iLine].GetWordWrapStringStartPosition(iWW - 1) + preferedPos;
                if (start.iChar > finish + 1)
                    start.iChar = finish + 1;
            }

            if (!shift)
                end = start;

            OnSelectionChanged();
        }

        internal void GoPageUp(bool shift)
        {
            if (preferedPos < 0)
                preferedPos = start.iChar - tb.lines[start.iLine].GetWordWrapStringStartPosition(tb.lines[start.iLine].GetWordWrapStringIndex(start.iChar));

            int pageHeight = tb.ClientRectangle.Height / tb.CharHeight - 1;

            for (int i = 0; i < pageHeight; i++)
            {
                int iWW = tb.lines[start.iLine].GetWordWrapStringIndex(start.iChar);
                if (iWW == 0)
                {
                    if (start.iLine <= 0) break;
                    start.iLine--;
                    iWW = tb.lines[start.iLine].WordWrapStringsCount;
                }

                if (iWW > 0)
                {
                    int finish = tb.lines[start.iLine].GetWordWrapStringFinishPosition(iWW - 1);
                    start.iChar = tb.lines[start.iLine].GetWordWrapStringStartPosition(iWW - 1) + preferedPos;
                    if (start.iChar > finish + 1)
                        start.iChar = finish + 1;
                }
            }

            if (!shift)
                end = start;

            OnSelectionChanged();
        }

        internal void GoDown(bool shift)
        {
            if (preferedPos < 0)
                preferedPos = start.iChar - tb.lines[start.iLine].GetWordWrapStringStartPosition(tb.lines[start.iLine].GetWordWrapStringIndex(start.iChar));

            int iWW = tb.lines[start.iLine].GetWordWrapStringIndex(start.iChar);
            if (iWW >= tb.lines[start.iLine].WordWrapStringsCount - 1)
            {
                if (start.iLine >= tb.lines.Count - 1) return;
                start.iLine++;
                iWW = -1;
            }

            if (iWW < tb.lines[start.iLine].WordWrapStringsCount - 1)
            {
                int finish = tb.lines[start.iLine].GetWordWrapStringFinishPosition(iWW + 1);
                start.iChar = tb.lines[start.iLine].GetWordWrapStringStartPosition(iWW + 1) + preferedPos;
                if (start.iChar > finish + 1)
                    start.iChar = finish + 1;
            }

            if (!shift)
                end = start;

            OnSelectionChanged();
        }

        internal void GoPageDown(bool shift)
        {
            if (preferedPos < 0)
                preferedPos = start.iChar - tb.lines[start.iLine].GetWordWrapStringStartPosition(tb.lines[start.iLine].GetWordWrapStringIndex(start.iChar));

            int pageHeight = tb.ClientRectangle.Height / tb.CharHeight - 1;

            for (int i = 0; i < pageHeight; i++)
            {
                int iWW = tb.lines[start.iLine].GetWordWrapStringIndex(start.iChar);
                if (iWW >= tb.lines[start.iLine].WordWrapStringsCount - 1)
                {
                    if (start.iLine >= tb.lines.Count - 1) break;
                    start.iLine++;
                    iWW = -1;
                }

                if (iWW < tb.lines[start.iLine].WordWrapStringsCount - 1)
                {
                    int finish = tb.lines[start.iLine].GetWordWrapStringFinishPosition(iWW + 1);
                    start.iChar = tb.lines[start.iLine].GetWordWrapStringStartPosition(iWW + 1) + preferedPos;
                    if (start.iChar > finish + 1)
                        start.iChar = finish + 1;
                }
            }

            if (!shift)
                end = start;

            OnSelectionChanged();
        }

        internal void GoHome(bool shift)
        {
            if (start.iLine < 0)
                return;

            start = new Place(0, start.iLine);

            if (!shift)
                end = start;

            OnSelectionChanged();

            preferedPos = -1;
        }

        internal void GoEnd(bool shift)
        {
            if (start.iLine < 0)
                return;

            start = new Place(tb.lines[start.iLine].Count, start.iLine);

            if (!shift)
                end = start;

            OnSelectionChanged();

            preferedPos = -1;
        }

        /// <summary>
        /// Set style for range
        /// </summary>
        public void SetStyle(Color foreColor)
        {
            SetStyle(foreColor, Color.Transparent, FontStyle.Regular);
        }

        /// <summary>
        /// Set style for range
        /// </summary>
        /// <remarks>Use Color.Transparent if you want to keep previous color.</remarks>
        public void SetStyle(Color foreColor, Color bgColor)
        {
            SetStyle(foreColor, bgColor, FontStyle.Regular);
        }

        /// <summary>
        /// Set style for range
        /// </summary>
        public void SetStyle(FontStyle fontStyle)
        {
            SetStyle(Color.Transparent, Color.Transparent, fontStyle);
        }

        /// <summary>
        /// Set style for range
        /// </summary>
        /// <remarks>Use Color.Transparent if you want to keep previous color.</remarks>
        public void SetStyle(Color foreColor, Color bgColor, FontStyle fontStyle)
        {
            //search code for style
            UInt16 code = tb.GetStyleCode(new Style(foreColor, bgColor, fontStyle));
            //set code to chars
            SetStyle(code);
            //
            tb.Invalidate();
        }

        /// <summary>
        /// Set style for given regex pattern
        /// </summary>
        public void SetStyle(Color foreColor, string regexPattern)
        {
            SetStyle(foreColor, Color.Transparent, FontStyle.Regular, regexPattern);
        }

        /// <summary>
        /// Set style for given regex pattern
        /// </summary>
        public void SetStyle(FontStyle fontStyle, string regexPattern)
        {
            SetStyle(Color.Transparent, Color.Transparent, fontStyle, regexPattern);
        }

        /// <summary>
        /// Set style for given regex pattern
        /// </summary>
        /// <remarks>Use Color.Transparent if you want to keep previous color.</remarks>
        public void SetStyle(Color foreColor, Color bgColor, string regexPattern)
        {
            SetStyle(foreColor, bgColor, FontStyle.Regular, regexPattern);
        }

        /// <summary>
        /// Set style for given regex pattern
        /// </summary>
        /// <remarks>Use Color.Transparent if you want to keep previous color.</remarks>
        public void SetStyle(Color foreColor, Color bgColor, FontStyle fontStyle, string regexPattern)
        {
            SetStyle(foreColor, bgColor, fontStyle, regexPattern, RegexOptions.None);
        }

        /// <summary>
        /// Set style for given regex pattern
        /// </summary>
        /// <remarks>Use Color.Transparent if you want to keep previous color.</remarks>
        public void SetStyle(Color foreColor, Color bgColor, FontStyle fontStyle, string regexPattern, RegexOptions options)
        {
            //search code for style
            UInt16 code = tb.GetStyleCode(new Style(foreColor, bgColor, fontStyle));
            //
            foreach(var range in GetRanges(regexPattern, options))
                range.SetStyle(code);
            //
            tb.Invalidate();
        }

        internal void SetStyle(UInt16 styleId)
        {
            //set code to chars
            int fromLine = Math.Min(End.iLine, Start.iLine);
            int toLine = Math.Max(End.iLine, Start.iLine);
            int fromChar = FromX;
            int toChar = ToX;
            if (fromLine < 0) return;
            //
            for (int y = fromLine; y <= toLine; y++)
            {
                int fromX = y == fromLine ? fromChar : 0;
                int toX = y == toLine ? toChar - 1 : tb.lines[y].Count - 1;
                for (int x = fromX; x <= toX; x++)
                {
                    Char c = tb.lines[y][x];
                    c.styleId = styleId;
                    tb.lines[y][x] = c;
                }
            }
        }
        
        /// <summary>
        /// Finds ranges for given regex pattern
        /// </summary>
        /// <param name="regexPattern">Regex pattern</param>
        /// <returns>Enumeration of ranges</returns>
        public IEnumerable<Range> GetRanges(string regexPattern, RegexOptions options)
        {
            //get text
            string text;
            List<Place> charIndexToPlace;
            GetText(out text, out charIndexToPlace);
            charIndexToPlace.Add(End);
            //create regex
            Regex regex = new Regex(regexPattern, options);
            //
            foreach (Match m in regex.Matches(text))
            {
                Range r = new Range(this.tb);
                //try get 'range' group, otherwise use group 0
                Group group = m.Groups["range"];
                if (!group.Success)
                    group = m.Groups[0];
                //
                r.Start = charIndexToPlace[group.Index];
                r.End = charIndexToPlace[group.Index + group.Length];
                yield return r;
            }
        }

        /// <summary>
        /// Clear all styles of range
        /// </summary>
        public void ClearStyle()
        {
            //set code to chars
            int fromLine = Math.Min(End.iLine, Start.iLine);
            int toLine = Math.Max(End.iLine, Start.iLine);
            int fromChar = FromX;
            int toChar = ToX;
            if (fromLine < 0) return;
            //
            for (int y = fromLine; y <= toLine; y++)
            {
                int fromX = y == fromLine ? fromChar : 0;
                int toX = y == toLine ? toChar - 1 : tb.lines[y].Count - 1;
                for (int x = fromX; x <= toX; x++)
                {
                    Char c = tb.lines[y][x];
                    c.styleId = 0;
                    tb.lines[y][x] = c;
                }
            }
            //
            tb.Invalidate();
        }

        void OnSelectionChanged()
        {
            if (tb.Selection == this)
                if (updating == 0)
                    tb.OnSelectionChanged();
        }

        /// <summary>
        /// Starts selection position updating
        /// </summary>
        public void BeginUpdate()
        {
            updating++;
        }

        /// <summary>
        /// Ends selection position updating
        /// </summary>
        public void EndUpdate()
        {
            updating--;
            if (updating == 0)
                OnSelectionChanged();
        }

        public override string ToString()
        {
            return "Start: " + Start + " End: " + End;
        }

        /// <summary>
        /// Exchanges Start and End if End appears before Start
        /// </summary>
        public void Normalize()
        {
            if (Start.iLine > End.iLine || (Start.iLine == End.iLine && Start.iChar > End.iChar))
            {
                var temp = start;
                start = end;
                end = temp;
            }
        }

        /// <summary>
        /// Expands range from first char of Start line to last char of End line
        /// </summary>
        public void Expand()
        {
            Normalize();
            start = new Place(0, start.iLine);
            end = new Place(tb.GetLineLength(end.iLine), end.iLine);
        }
    }
}
